package com.moonshineproject.atomic.parser
{
	import com.moonshineproject.atomic.EditorTextArea;
	import com.moonshineproject.atomic.multiplayer.CocomoComponents;
	import com.moonshineproject.atomic.multiplayer.PsuedoTime;
	
	import flash.events.EventDispatcher;
	import flash.events.KeyboardEvent;
	import flash.geom.Rectangle;
	import flash.text.TextFormat;
	import flash.ui.Keyboard;
	/*
		Inefficient string based actionscript parser/highlighter. If you can do a real parser/lexer, please do.
		Keeps 'state' as a string equal in length to the text. 
		The metadata string contains a char for each char in the text (code), with types defined by MD_ consts (see below here).
	*/
	
	[Event(name="tabDiff", type="DiffEvent")]
	
	public class FauxParser extends EventDispatcher
	{
		
		import mx.controls.Alert;
		import mx.core.UIComponent;
	
	
		public var editor:EditorTextArea;
		public var multiplayer:CocomoComponents;
	
		protected const NOT_FOUND:int = -1;
		protected const SPACE:String = ' ';
		protected const LEFT:uint = 0;
		protected const RIGHT:uint = 1;
	
		protected const MD_UKNOWN:String = ".";
		protected const MD_ACTIONSCRIPT:String = "a";
		protected const MD_COMMENT:String = "/";
		protected const MD_BLOCK_COMMENT:String = "*";
		protected const MD_STRING:String = "s";
		protected const MD_DSTRING:String = "S";
		protected const MD_END:String = "E";
		
		protected const TRIGGER_DSTRING:String = '"';
		protected const TRIGGER_COMMENT:String = '//';
		protected const TRIGGER_COMMENT_END:String = String.fromCharCode(13);
		protected const TRIGGER_BLOCK_COMMENT_START:String = '/*';
		protected const TRIGGER_BLOCK_COMMENT_END:String = '*/';
		
		protected var MD_DEFAULT:String = MD_ACTIONSCRIPT;
		
		private var contextTriggers:RegExp = new RegExp(
														TRIGGER_DSTRING + '|' +
														TRIGGER_COMMENT + '|' + 
														TRIGGER_COMMENT_END
														, 'g');
		
		// Used for AS3 highlighting
		private var triggerCharRegExp:RegExp = /([;\s\n:(\)\{\}\.])/g;
		
		// Parse full line on each change (for simplicity)
		private var triggerChars:Array = ["\n"];
		
		public var lastCharCount:Number = 0;
		private var lastPosition:Number = 0;
	
		private var lastChar:String = "";;
	
		private var definitions:Array = [
											{keywords: ['is', 'if', 'in', 'as', 'new', 'for', 'use', 'set', 'get', 'try', 
														'null', 'true', 'void', 'else', 'each', 'case', 'this', 
														'false', 'const', 'catch', 'class', 'returnriva', 'switch', 
														'static', 'private', 'public', 'override',
														'protected', 'namespace'],
											 color: 0x00a2ff},
											 {keywords: ['function'],
											 color: 0xa6c6da},
											 {keywords: ['var'],
											 color: 0xa6c6da},
										 ];

		private var syntaxSets:Object = {};
			
		public var metadata:String = "";
	
		
		public function init():void {
			// Generate coloring lookup table
			var tempLengthBased:Object = {};
			
			for (var i:int = 0; i < definitions.length; i++) {
				var set:Object = definitions[i];
				for (var j:int = 0; j < set.keywords.length; j++) {
					var tkeyword:String = set.keywords[j];
					var tcolor:uint = set.color;
					if (!tempLengthBased[tkeyword.length])
						tempLengthBased[tkeyword.length] = new Array();
						
					tempLengthBased[tkeyword.length].push({value: tkeyword, color: tcolor});
				}
			}
			
			syntaxSets = tempLengthBased;
			
			// Set up textarea
			editor.getTextField().wordWrap = false;
			editor.getTextField().defaultTextFormat = new TextFormat(null, null, 0x0);
			
			editor.addEventListener(KeyboardEvent.KEY_DOWN, trapTab, true, 0);

			//editor.parent.focusManager.setFocus(editor);
		}
		
		// TODO: do retStr+=retStr for less iterations?
		private function getFillerString(length:int):String {
			var retStr:String = "";
			for (var i:int = 0; i < length; i++) {
				retStr += ".";
			}
			return retStr;
		} 
		
		public function handleChange(selectionStart:int, selectionStop:int, remote:Boolean=false):Diff {

			//var selectionStart:int = editor.selectionBeginIndex;
			//var selectionStop:int = editor.selectionEndIndex;
			
			var selectionBeginIndex:int = selectionStart;
			var selectionEndIndex:int = selectionStop;
			
			var diff:int = editor.text.length - lastCharCount;
			lastCharCount = editor.text.length;
			
			// Special case handling
			if (editor.text.length == 0) {
				metadata = "";
				return new Diff();
			}
				
			// Fill metadata with dots for the changes
			if (diff == 1) { // One keypress
				metadata = metadata.substr(0, selectionStart-1) + '.' + metadata.substr(selectionStart-1, metadata.length);
			} else if (diff > 1) { // Paste
				metadata = metadata.substring(0, selectionStart-diff) + 
						   getFillerString(diff) +
						   metadata.substring(selectionStop-diff, metadata.length);
			} else if (diff == -1) { // Backspace
				metadata = metadata.substr(0, selectionStart-1) + metadata.substr(selectionStart);
			} else if (diff < -1) { // Paste on selection or Cut
				metadata = metadata.substr(0, selectionStart-(diff*-1)) + metadata.substr(selectionStart);
			}

			var newChar:String;
			var lastChar:String;
			
			var editStart:int;
			var editEnd:int;

			if (diff > 1)
				editStart = searchLeft(editor.text, selectionBeginIndex-diff, triggerChars);
			else
				editStart = searchLeft(editor.text, selectionBeginIndex-1, triggerChars);
			
			editEnd = searchRight(editor.text, selectionEndIndex, triggerChars);

			if (editStart == editEnd) 
				editStart-=1;
				
			lastPosition = editStart;

			
			// Ad hoc debuggin help
			var startRect:Rectangle = editor.getTextField().getCharBoundaries(editStart+1);
			var endRect:Rectangle = editor.getTextField().getCharBoundaries(editEnd-1);
			
			//debugArea.graphics.clear();
			//debugArea.graphics.beginFill(0x0, 0.2);
			//if (startRect)
			//	debugArea.graphics.drawRect(startRect.x-1, startRect.y, startRect.width+1, startRect.height+1);
			//debugArea.graphics.endFill();
			//debugArea.graphics.beginFill(0x0, 0.8);
			//if (endRect)
			//	debugArea.graphics.drawRect(endRect.x-1, endRect.y, endRect.width+1, endRect.height+1);
			//debugArea.graphics.endFill();
			
			
			
			lastChar = editor.text.substring(selectionBeginIndex-1, selectionBeginIndex);
			// Is this really needed?
			if (lastChar == "\r")
				editStart = searchLeft(editor.text, editStart, triggerChars);
				
				
			invalidateMetadata(editStart, editEnd);


			if (editor.text.length != metadata.length) {
				//Alert.show("Very Bad Karma: \rMetadata length not matching text length");
			}

			// Invalidates cursor color
			editor.getTextField().setFocus();
			
			var d:Diff = new Diff();
			d.diff = diff;
			if (diff > 0) { // TODO: Handle select-and-keydown or select-and-paste for both diff > 0 & diff < 0
				d.change = editor.text.substring(selectionBeginIndex-diff, selectionEndIndex);
				d.start = selectionStart-diff;
				d.end = selectionStop-diff;	
			} else {
				d.change = removedChars;
				d.start = selectionStart;
				d.end = selectionStop-diff;
			}
			
			return d;
		}
		

		private function invalidateMetadata(start:int, end:int):void {
			
			if (start == end) return;
			var searchString:String = editor.text.substring(start, end);
			
			trace("invalidateMetadata("+start+", " + end + ")");
			
			var matches:Array = [];
			var matchChars:Array;
			var startIndex:int = 0;
			
			matches = searchString.split(contextTriggers);
			
			var currentContext:String
			//lastMatch.text = searchString;
			// If no context switches occur
			if (matches.length <= 1) {
				invalidateUnswitched(start, end); 
				return;
				
				if (start < 0) start = 0;
				
				currentContext = getCurrentContext( start-1 );
				
				lastMatch.text += " No switches. ";
				
				if (currentContext == MD_UKNOWN)
					currentContext = MD_DEFAULT;
				else if (currentContext == MD_END) // Closing a context
					currentContext = metadata.charAt(start);
				else if (currentContext == '')
					currentContext = MD_DEFAULT;
				
				replaceMetaData(start, end, currentContext);
				invokeHighlighter(currentContext, start+1, end);
				return;
			} else {
				matchChars = searchString.match(contextTriggers);
				// Update section before first context switch (due to always grabbing more than we need)
				if (matches[0].length > 0) {
					var firstMatch:int = searchString.indexOf( matchChars[0] );
					if ( firstMatch != 0 )						
						invalidateMetadata(start, start+matches[0].length);
				}
			}

			//lastMatch.text += " There be context switches: " + matchChars.toString() + " ";
			
			var splitString:String;
			var currentTrigger:String;
			
			var mStart:int = start;//matches[0].length + start;
			//var mStart:int = matches[i].startIndex + start;
			for (var i:int = 0; i < matchChars.length; i++) {

				currentTrigger = matchChars[i];
					
				mStart += matches[i].length;
				
				currentContext = getCurrentContext(mStart-1);

				switch ( currentTrigger) {
					case TRIGGER_DSTRING:
						if ( currentContext == MD_ACTIONSCRIPT ) { // String context start inside code context: start string context.

							splitString = searchString.substring(mStart+1-start, searchString.length);
							var matchingEnd:Number = editor.text.indexOf(TRIGGER_DSTRING, mStart+1);

							if (matchingEnd == -1)
								matchingEnd = metadata.length;
							//else
							//	matchingEnd += mStart+1;	

							replaceMetaData(mStart, matchingEnd, MD_DSTRING);		
							invokeHighlighter(MD_DSTRING, mStart+1, matchingEnd);
							
							// Do we have a closing string
							if ( matchChars.indexOf(TRIGGER_DSTRING, i+1) == -1 ) {
								trace("need to reparse file due to open string.");
								invalidateMetadata(mStart+1, metadata.length);
								return;
							}

							// TODO: fast-forward i to the matched closing ", if any
						}
						else if (currentContext == MD_DSTRING) { // String context marker inside string context: end string context.

							var parentContext:String = getParentContext(mStart);

							metadata = metadata.substring(0, mStart) + 
									   MD_END +
									   metadata.substring(mStart+1, end).replace(/./g, parentContext) +
									   metadata.substring(end, metadata.length);
							
							invokeHighlighter(MD_DSTRING, mStart, end);
							
							// TODO: Optimize this somehow
							invalidateMetadata(mStart+1, metadata.length);
							return;
						}
						break;
					case TRIGGER_COMMENT:
						if (currentContext == MD_ACTIONSCRIPT) {
							splitString = searchString.substring(mStart+1-start, searchString.length);
							matchingEnd = splitString.indexOf( String.fromCharCode(13) );

							matchingEnd = editor.text.indexOf("\r", mStart);
							
							//trace("matchingEnd: " + matchingEnd);
							
							if (matchingEnd == -1)
								matchingEnd = metadata.length;
							
							replaceMetaData(mStart, matchingEnd, MD_COMMENT);
							invokeHighlighter(MD_COMMENT, mStart+1, matchingEnd);
						}
						break;
					case TRIGGER_COMMENT_END:
						var offset:int = 0;
						if (currentContext == MD_COMMENT) {
							// Mark comment end
							replaceMetaData(mStart, mStart+1, MD_END);
							offset++;
						}

						replaceMetaData(mStart+offset, mStart+1, currentContext);
						// No highlight needed for a \r, not a visible char?
							
						if (i+1 < matchChars.length) {
							invalidateUnswitched(mStart+1, mStart+1+matches[i+1].length);
						} else {
							invalidateUnswitched(mStart+1, end);
						}

						break;
				} // end switch case
				mStart += matchChars[i].length;
			} // end word loop
			
			
			// Invalidate trailing chars, if any ( "asdf"; foo, ;foo is trailing after the last " )
			invalidateMetadata(mStart, end);
			return;
		}
		
		private function invalidateUnswitched(start:int, end:int):void {
			if (start < 0) start = 0;
				
			var currentContext:String = getCurrentContext( start-1 );
			
			//lastMatch.text += " No switches. ";
			
			if (currentContext == MD_UKNOWN)
				currentContext = MD_DEFAULT;
			else if (currentContext == MD_END) // Closing a context
				currentContext = metadata.charAt(start);
			else if (currentContext == '')
				currentContext = MD_DEFAULT;
			
			replaceMetaData(start, end, currentContext);
			invokeHighlighter(currentContext, start+1, end);
		}
		
		private function replaceMetaData(start:int, stop:int, newMD:String):void {
			metadata = metadata.substring(0, start) + 
					   metadata.substring(start, stop).replace(/./g, newMD) +
					   metadata.substring(stop, metadata.length);
		}
		
		private function getCurrentContext(start:int):String {
			var context:String = metadata.charAt(start);

			if (context == MD_END) 
				return getParentContext(start);
			else if (context == MD_UKNOWN)
				return getCurrentContext(start-1);
			else if (context == "")
				if (start <= 0)
					return MD_DEFAULT;
				else
					context = getCurrentContext(start-1);
			
			return context;
		}
		
		private function getParentContext(start:int, allowUnknown:Boolean = false):String {
			var context:String = metadata.charAt(start);

			if (context == MD_END) 
				return getParentContext(start-1, allowUnknown);
			if (!allowUnknown && context == MD_UKNOWN)
				return getParentContext(start-1, allowUnknown);
			
			for (var i:int = start; i > 0; i--) {
				if (metadata.charAt(i) != context) {
					
					if (metadata.charAt(i) == MD_END)
						return getParentContext(i-1);
					//trace("wanna return this as parent context: " + metadata.charAt(i));
					return metadata.charAt(i);
				}
			}
			
			return MD_DEFAULT;
		}
		
		private var highlighters:Object = { 'a': asHighlight,
											'S': stringHighlight,
											'/': commentHighlight };
		
		private function invokeHighlighter(context:String, start:int, end:int):void {
			if (start < 0) start = 0;
			if (end == -1) end = editor.text.length-1;
			if (end > editor.text.length) end = editor.text.length;
			if (start > end) return; 
			if (highlighters[context])
				highlighters[context](start-1, end);
		}
		
		private function commentHighlight(start:int, end:int):void {
			editor.getTextField().setTextFormat( new TextFormat(null, null, 0x6bcf64), start, end );
		}
		
		private function stringHighlight(start:int, end:int):void {
			editor.getTextField().setTextFormat( new TextFormat(null, null, 0xaa2c2c), start, end );
		}
		
		private function asHighlight(start:int, end:int):void {
			////trace("asHighlight: " + editor.text.substring(start, end));
			var m:Array;
			
			editor.getTextField().setTextFormat( new TextFormat(null, null, 0x555555), start, end );
			var words:Array = editor.text.substring(start, end).split(triggerCharRegExp);
			var newWord:String;
			var match:Boolean;
			var offset:int = 0;
			for (var j:int = 0; j < words.length; j++) {
				newWord = words[j];
				if (newWord == "\r")
					offset += 1;
				else
					offset += newWord.length;
				if (!syntaxSets[newWord.length]) continue;
				var loadedSet:Array = syntaxSets[ newWord.length ];
				for (var i:int = 0; i < loadedSet.length; i++) {
					match = false;
					if (newWord == loadedSet[i].value) {
						var tf:TextFormat = new TextFormat(null, null, loadedSet[i].color);
						if (newWord == "\r")
							editor.getTextField().setTextFormat(tf, start+offset-1, start+offset);
						else
							editor.getTextField().setTextFormat(tf, start+offset-newWord.length, start+offset);
						match = true;
					}
				} // end inner for
			} // end top for
		}

		private function searchLeft(text:String, startingPoint:int, forChars:Array):int {
			var pos:int = text.lastIndexOf("\r", startingPoint);
			
			if (pos < 0) return 0;
			return pos;
		}
		
		private function searchRight(text:String, startingPoint:int, forChars:Array):int {
			var pos:int = text.indexOf("\r", startingPoint);
			
			if (pos < 0) return text.length;
			return pos+1;
		}
		
		private var removedChars:String;
		
		private function trapTab(event:KeyboardEvent):void {
			if (event.keyCode == Keyboard.TAB) {
				// TODO: handle moving entire sections around
				if (editor.selectionBeginIndex == editor.selectionEndIndex) {
					
					var d:Diff = new Diff();
					d.psuedoTime = PsuedoTime.getInstance().getPsuedoTimestamp();
					d.diff = 1;
					d.start = editor.selectionBeginIndex;
					d.end = editor.selectionEndIndex;
					d.change = "\t";
					var e:DiffEvent = new DiffEvent('tabDiff', d);
					dispatchEvent(e);
					
					editor.getTextField().replaceText(editor.selectionBeginIndex, editor.selectionBeginIndex, '\t');
					var context:String = getCurrentContext(editor.selectionBeginIndex);
					metadata = metadata.substr(0, editor.selectionBeginIndex-1) + 
							   context + 
							   metadata.substr(editor.selectionBeginIndex-1, metadata.length);
					lastCharCount = editor.text.length;
					
					editor.selectionBeginIndex++;
					editor.selectionEndIndex++;
				}
			} else if (event.keyCode == Keyboard.BACKSPACE) {
				if (editor.selectionBeginIndex == editor.selectionEndIndex)
					removedChars = editor.text.substring(editor.selectionBeginIndex-1, editor.selectionEndIndex);
				else
					removedChars = editor.text.substring(editor.selectionBeginIndex, editor.selectionEndIndex);
			}
		}

	}
}